数据结构与算法入门(2)--预备知识(指针、结构体、动态内存分配)

来源:互联网 发布:java解压pkg 编辑:程序博客网 时间:2024/05/17 03:04

我们知道程序=数据的存储+数据的操作+计算机程序设计语言。 想要实现典型的数据结构,需要选择一门合适的编程语言。个人认为,C语言是一个很好的工具。C中的指针能很好的实现链表以及以链表为基础的树与图等。一些高级语言,如java,python没有指针的概念,实现典型的数据结构难免会有些变味。既然选择了C语言,那就讲讲想要用C语言实现数据结构需要的预备知识。主要分为三部分– 指针,结构体和动态内存分配

本节知识框架

(Firefox浏览器,右键点击图片,选择view image即可查看高清大图。)


  • 指针

    • 指针的基本概念

      • 指针与指针变量
        变量在内存中分配到的位置叫做地址,也就是指针指针就是地址,地址就是指针。
        指针变量 :存储另一个变量的地址的变量。(指针变量是通过寻址来找到另一个变量)
      • 指针变量的访问
        • 直接访问
          按变量地址来存取变量值
        • 间接访问
          通过存放变量地址的变量来访问变量
      • 指针变量的定义与初始化
        【存储类型】 数据类型 *指针名 = 地址;
        • 指针的初始化
          除了上述的,用变量的地址来初始化指针变量之外,还有三种方法来初始化指针变量。
          1. 通过指针变量初始化指针变量
          2. 通过整型地址常量来初始化指针变量
            int *p;
            p = (int *) 0x23;
          3. NULL或0
      • 基类型 与指针变量的大小
        指针变量所指向的变量的数据类型为指针的基类型。尽管指针变量可以是不同的基类型,但指针变量的大小是固定的,是由计算机硬件所决定的。对32位系统来说,指针变量的大小为4个字节。想一想为什么?(因为32位操作系统地址总线宽度为32位,用4个字节可以遍历所有内存空间。又因指针就是地址,所以指针的取值范围与地址范围相同,故用4个字节可以表示所有可能的指针值。)
        例如:
        float *a ;
        int *b;
        上述两个语句定义了两个指针变量。第一个指针变量指向一个float类型的变量,第二个指针变量指向一个int类型变量。指针变量a的基类型为float,指针变量b的基类型为int。但是a和b的大小均为4个字节。(注意,int数据类型也为4个字节,所以可以通过整型地址常量来初始化指针变量)
    • 常见运算—指针的运算就是地址的运算

      • 赋值运算
        例如:int a; int *p=&a; 这两行代码表示:
        1. 定义了一个整型变量a
        2. 定义了一个指针变量p且指针变量p只能指向整型变量
        3. 指针变量p通过赋值运算=指向了整型变量a
      • 指针的移动
        指针变量存放的是地址,通过改变指针变量的值,能够使指针指向不同的地址。这个改变指针变量的值的过程,就叫做指针的移动。这个时候就有一个问题了,指针变量的移动的基本单位是多大?是一个字节吗?这里我们介绍两个概念:存储单元和字节
        • 字节
          我们知道,在主存储器也就是内存中,数据是以字节为单位进行存储的。一个字节有8位。一个字节对应一个地址。
        • 存储单元
          存储单元是一个跟数据类型(data_type)紧密相连的一个概念。我们知道,不同的数据类型定义的变量所分配的字节数不同。 比如C语言中,char 变量占一个字节,int 变量占4个字节,float变量占4个字节,double变量占8个字节。那么对应的,char变量的存储单元大小为一个字节,int变量的存储单元大小为4个字节… 对int *p=8000H,执行完++p后,p的值为8004H而不是8001H。因为指针移动的基本单位是存储单元
      • 引用指针变量的值和指针变量所指变量的值
        int a,*p=&a;
        通过取地址运算符&来引用指针变量的值 p==&a (true)
        通过指针运算符*来引用指针变量所指变量的值 a==*p (true)
      • 比较两个同类型指针的值是否相等
        若相等则说明这两个指针指向同一变量
      • 求两指针变量的差
        两指针所指位置间相差元素个数。
    • 指针与数组的关系
      • 指针变量既可以指向数组也可以指向数组的元素
        例如:

        int a[20],*p,*q;
        p=a;
        q=&a[0];
      • 数组元素的表示方法
        • 指针法 *(p+1)=1 // a[1]=1
        • 下标法 p[1] =1
      • 数组名是指针常量,表示数组的首地址。即&a[0]。
        所以数组名不能进行自增,自减和赋值等操作。而指向数组的指针变量则可以进行上述操作。
      • 二维数组地址表示
        • a—二维数组首地址,即第0行地址
        • a+i 第i行的地址
        • *(a+i)= a[i] 第i行第0列元素的地址
        • *(a+i)+j= a[i]+j 第i行第j列元素的地址
    • 数组指针
      通过指向一维数组的指针变量引用二维数组。常用于处理二维数组。

      • 定义:
        [存储类型] 数据类型 (* 指针变量名)[二维数组列数/一维数组维数]
      • 例:
        int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
        int (*p)[4]=a;
        则有 p[2]=3,(*(p+2)+3)=12;
        如何理解上述两段代码?
        1. 定义了一个二维数组a,三行四列;
        2. 定义了一个数组指针p。为什么该指针叫做数组指针?观察p的定义语句:int (*p)[4]。将()里的内容当做一个整体则上式变为int x[4],(*p)=x;即,int (*p)[4]先定义了一个数组,后定义了一个指针。故称为数组指针。此时,p为行指针。对于p,存储单元为4个int型变量即16个字节。p+1移动16个字节的地址。
        3. 数组指针的元素个数要与二维数组的列数相同。即若定义,int a[3][x],int (*p)[y],则x=y。
    • 指针数组
      每一个元素都是指针,分别指向同一数据类型的变量。常用于处理字符串。

      • 定义:
        数据类型 *指针变量名 [数组长度]
      • 例:
        int *p[4];
        *p=”abcd”; // p[0]=”abcd”
        or
        int *p[4]={“abcd”};

        字符指针和字符数组的区别?
        对字符指针 int *p=”abcd”; 等价于 int *p; p = “abcd”;
        而对字符数组 int a[20] =”abcd”; 拆开写成 int a[20]; a=”abcd’;就不对了。
        这就是两着的最主要的区别。

    • 指针与函数
      1. 指针变量作为函数形参
        传入的实参必须为 地址 或 指针变量
      2. 指针变量作为函数实参
        地址传递 vs 值传递
        问题:如何通过被调函数修改主调函数中变量的值?传值?传地址?传引用(C++中)?
      3. 函数名赋给指针变量—函数指针
        函数指针存放的是函数代码段的入口地址

总之,用一句话总结指针,那就是:通过一个变量A,能够引用另一个变量B的地址与地址对应存储单元所存储的值,那么这个变量A就是指针变量,获取到的另一个变量B的地址就是指针。


  • 结构体
    • 结构体定义
      • 为什么要有结构体?
        C语言中数组中的变量必须要有相同的数据类型和长度,灵活性低。于是出现了一种可自定义的构造数据类型,即为结构。
      • 一般形式
        struct 结构体名
        {
        数据类型1 结构体成员变量名1;
        数据类型2 结构体成员变量名2;

        } ;
        如何理解上述定义?
        1. struct 是关键字 表示定义一个结构体数据类型
        2. 结构体名和结构体成员变量名为用户定义的标识符
        3. 结构体最后大括号外要有分号。(为什么?因为这是一个语句statement,起声明的作用。程序的入口为main函数,当编译器在main函数中读到struct 结构体名时,编译器会查找main函数前定义的声明。这个statement告诉编译器这里定义了一个数据类型,数据类型为struct 结构体名。 然后编译器就能正常的完成编译。与宏定义(#define PI 3.1415926535)形成对比{无分号},与函数定义int max (int a[]);可以形成类比{有分号}
        4. Tips: 不同结构体中成员变量名可以重名,结构体中成员变量可以和主函数或者其他函数中变量重名。
    • 结构体类型变量的定义 — 结构体变量,结构体指针,结构体数组
      • 声明结构体类型,再定义结构体变量
        struct stu
        {

        };
        struct stu anran;
      • 声明结构体类型的同时定义结构体变量
        struct stu
        {

        }anran;
      • 直接定义结构体类型变量
        struct
        {

        }anran;
    • 结构体变量中的数据引用
      • 对结构体变量中的成员的引用与操作
        struct stu
        {
        int age;
        }anran,*panran;
        • 结构体变量名.成员名 // anran.age
        • 指针变量名->成员名 // panran->age
        • (*指针变量名).成员名 // (*panran).age
      • 结构体变量进行整体赋值
        • 结构体与数组的区别
          1. 结构体中可以含有不同的数据类型
          2. 结构体变量可以相互赋值,而数组无法相互赋值
            why?
            数组不是一个数据类型,是单一数据类型的一个集合。
            数组名是一个指针常量,无法赋值。
      • 函数之间结构体变量的数据传递
        • 函数传递结构体变量
        • 函数传递结构体变量成员
        • 传递结构体地址
        • 返回值是结构体类型
        • 返回值是指向结构体类型变量的指针

  • 动态内存分配
    在讲动态内存分配之前,我们先来思考一个问题:如何跨函数使用内存? 即,如何通过被调函数,开辟一块内存空间,当被调函数执行结束后,仍然能够访问该内存空间?
    答案是使用memory allocation function.即malloc()函数。malloc函数返回类型是开辟内存空间的首地址,以字节为单位。我们知道在C中,int类型存储单元为4个字节。所以malloc动态分配的内存必须要经过强制类型转换才能成为能存放int类型变量的内存空间。所以通常我们如下使用malloc函数。
    int *p;
    p = (int *) malloc (sizeof(int)*number);

    C/C++中没有垃圾回收机制,也就是说,分配的动态内存在调用结束后不会自动销毁,必须通过free()函数来销毁。而在JAVA中存在垃圾回收机制,无需考虑内存回收的问题。利用这个特性,我们便可以实现跨函数使用内存的功能。与之相对的是静态内存分配。静态内存分配的内存空间在函数结束后便会自动销毁。
    比如上述两个语句,当被调函数执行完毕后,指针变量p便被销毁,但动态分配的number个int类型变量的内存不会被销毁,我们只要把p指针的值return回主函数,便能跨函数使用内存。


最后用一个小程序来结束我们的结构体和动态内存分配的话题

# include <stdio.h># include <malloc.h>struct Student{    int sid;    int age;};struct Student * CreateStudent(void);void ShowStudent(struct Student *);int main(void){    struct Student * ps;    ps = CreateStudent();    ShowStudent(ps);    return 0;}void ShowStudent(struct Student * pst){    printf("%d %d\n", pst->sid, pst->age);}struct Student * CreateStudent(void){    struct Student * p = (struct Student *)malloc(sizeof(struct Student));    p->sid = 99;    p->age = 88;    return p;}
0 0
原创粉丝点击