美文网首页
Android NDK 3 C语言数组与指针

Android NDK 3 C语言数组与指针

作者: seraphzxz | 来源:发表于2018-07-06 14:15 被阅读6次

    概述

    C 语言的数组是一种将标量数据聚集成更大数据类型的方式。其实现的方式非常简单,很容易翻译为机器代码。C 语言中一个不同寻常的特点是可以产生指向数组中元素的指针,并对这些指针进行运算。

    一、数组

    1.1、基本原则

    数组是一组数目固定、类型相同的数据项,数组中的数据项被称为元素。数组的声明方式如下:

    T A[N];
    

    其中 T 为数据类性,N 为整形常数。首先,它在内存中分配一个 L*N 字节的连续区域,其次它引入了标识符 A,可以用 A 来作为指向数组开头的指针。

    1.2、指针运算

    C 语言允许对指针进行运算,计算出来的值会根据该指针类型的大小进行伸缩。例如,p 是一个指向类型为 T 的数据的指针,p 的值为 x,那么表达式 p+i 的值为
    x+L*i,这里 L 为数据类型 T 的大小。

    单操作数操作符“&”和“*”可以产生指针和间接引用指针。

    1.3、数组初始化

    可以给数组元素指定初始值,例如:

    double values[7] = {1.0,1.2,1.5};
    

    这里初始值个数小于数组元素个数,那么没有初始值的元素就设置为0。如果初始值个数大于数组元素个数,那么就会报错。

    1.4、多维数组

    当我们创建数组的数组时,即创建多维数组,数组分配和引用的一般原则也是成立的。例如声明以下多维数组:

    int A[5][3];
    

    等价于下面的声明:

    typedef int row3_t[3];
    row3_t A[5];
    

    整个数组的大小为:4×5×3=60字节。

    数组A可以看做一个5行3列的矩阵,数组元素按照“行优先”的顺序排列,这就就意味着第0行的所有元素,可以写做A[0]。这种排列顺序是嵌套声明的结果。将 A 看作一个5个元素的数组,每个元素都是3个 int 的数组。

    要访问多维数组的元素,编译器会以数组启始为基地址,偏移量为索引(需要经过伸缩),产生计算期望的偏移量。通常来说,对于一个声明如下的数组:

    T D[R][C];
    

    它的元素 D[i][j] 的内存地址为:

    &D[i][j] = x + L(C × i + j)  // x 为多维数组首地址
    

    1.5、变长数组

    历史上 C 语言只支持在编译时就能确定的多维数组(对一维可能有例外)。ISO C99 引入了一种功能,允许数组的维度是表达式,在数组被分配的时候才计算出来。

    看下面这个例子:

    size_t size = 0;
    printf("Enter the number of elements you want to store: ");
    sacanf("%zd",&size)
    float values[size];
    printf("The size of variable array, values is %zu bytes.\n",sizeof values);
    

    执行结果如下:

    Enter the number of elements you want to store: 5
    The size of variable array, values is 20 bytes.
    

    上面代码逻辑很简单,就是把从键盘上读到的值放到 size 中,接着使用 size 的值指定数组的长度。这里要注意的是,因为 size_t 是用实现代码定义的整数类型,所以如果使用 %d 读取这个值,就会得到一个编译错误。%zd 中的 z 告诉编译器,它应该是 size_t,所以无论整数类型 size_t 是什么,编译器都会使说明符合使用于读取操作。

    二、指针

    指针对于 C 语言来说太重要,然而,想要全面理解指针,除了要对 C 语言有熟练的掌握外,还要有计算机硬件以及操作系统等方方面面的基本知识。所以指针对初学不是十分友好,所以对于初学者选择好的入门参考书非常重要,这里推荐一下 Ivor Horton 的《Beginning C》,Kenneth A. Reek 的《Pointers on C》和 K&R 的《The C Programming Language》(这本其实不适合初学者)。

    指针是一种保存变量地址的变量。在 C 语言中,指针的使用非常广泛,原因之一是,指针常常是表达某个计算的惟一途径,另一个原因是,同其它方法比较起来使用指针通常可以生成更高效、更紧凑的代码。

    一、指针与地址

    指针是能够存放一个地址的一组存储单元,指针的值实质是内存单元(即字节)的编号,所以指针单独从数值上看,也是整数,他们一般用16进制表示。指针的值(虚拟地址值)使用一个机器字的大小来存储,也就是说,对于一个机器字为 w 位的电脑而言,它的虚拟地址空间是0~2的w次方 - 1,程序最多能访问2的w次方个字节。这就是为什么 32 位系统最大支持 4GB 内存的原因了。

    在 Beginning C 中有如下描述“可以存储地址的变量称为指针(pointer),存储在指针中的地址通常是另一个变量。”结合下图可以更直接的理解这句话。

    pointer工作原理.png

    上图中,指针 pnum 含有另一个变量 num 的地址,变量 num 是一值为 99 的整数变量。存储在 pnum 中的地址是 num 的第一个字节的地址。“指针”这个词也用于
    表示一个地址。但是仅仅知道 pnum 是一个指针是不够的,编译器必须要知道指针所指向的变量的类型,才能正确的处理指针指向内存的内容。从上一篇文章我们知道,
    一个 char 类型占用一个字节,而1个 int 类型或占用4个字节。所以说,每个指针都和具体的变量类型相关联,并且也只能用于指向该类型的变量。

    一般给定义类型的指针写成 type,其中 type 是任意给定的类型。这里要注意,类型名 void 表示没有指定类型,所以 void 类型的指针可以包含任意类型的数据
    项的地址。

    2.1、声明指针

    指针的声明形式和变量的声明形式类似,声明一个指向 int 类型的变量的指针声明如下:

    int* pnumber; 或 int *pnumber;
    

    pnumber 变量的类型为 int*,可以存储任意 int 类型变量的地址。在编写代码时最好统一指针的声明方式。看下面这个例子:

    int *p, q;
    

    上述语句声明了一个指针 p 和一个变量 q,两者都是 int 类型。

    上面的例子中虽然创建了 pnumber 变量,但是并没有对它进行初始化,这种做法很危险,因为未初始化的内存中可能存在垃圾值,在程序执行可能会带来意想不到的问
    题。所以应该总是在声明指针时对他进行初始化。以下代码表示 pnumber 不指向任何对象:

    int *pnumber = NULL;
    

    NULL 是在标准库中定义的一个常量,对于指针它表示 0。

    2.2 寻址运算符

    可以使用寻址运算符 & 来获取变量的地址。例如:

    int number = 99;
    int *pnumber = &number;
    

    2.3、通过指针访问值

    使用间接运算符“*”可以访问指针的变量值,该操作符也称为“取消引用运算符”(dereferencing operator),它用于取消对指针的引用。看下面示例:

    int number = 1024;
    int *pointer = &number;
    
    printf("The value of number is: %d.\n", number);
    printf("The value of number is: %d.\n", *pointer);
    printf("The address of number is: %p.\n", pointer);
    printf("The address of pointer is: %p.\n", &pointer);
    printf("The size_t of address is: %zd bytes.\n", sizeof(pointer));
    

    执行结果:

    The value of number is: 1024.
    The value of number is: 1024.
    The address of number is: 0x7fffc066958c.
    The address of pointer is: 0x7fffc0669590.
    The size_t of address is: 8 bytes.
    

    2.4、指向常量的指针

    声明指针时可以使用关键字 const 进行指定,表示该指针指向的值不能被修改。如下所示:

    int value = 100;
    const int* pvalue = &value;
    

    以上语句是将指针 pvalue 所指向的值声明为常量,编译器会检查是否有语句试图改变 pvalue 指向的值,并将这些语句标记为错误。以下语句就会产生这样一个错误:

    *pvalue = 101; // error: assignment of read-only location ‘*pvalue’
    

    但是可以通过以下语句修改 value 的值:

    value = 101; // 合法
    

    由于指针不是常量,可以修改指针 pvalue 的值:

    int number = 0;
    pvalue = &number;
    

    但是仍旧不能使用指针改变该变量的值,总结来说就是指向常量的指针不能通过指针来改变该指针指向的值

    2.5、常量指针

    也可以使指针中储存的地址不能被改变,其声名方式如下:

    int value = 100;
    int *const pvalue = &value; // 声明常量指针
    

    编译器会检查是否有语句试图改变 pvalue 的值,并将这些语句标记为错误。如下所示:

    pvalue = &number; // error: assignment of read-only variable ‘pvalue’
    

    但是可以修改指针指向的值:

    *pvalue = 101; // 合法
    value = 101; // 合法
    

    由以上可以看出,在对常量指针声明时需要为该指针指定一个有效的地址,以避免出错。

    可以创建一个常量指针,它指向一个常量值:

    const int *const pvalue = &value;
    

    以上语句既不能改变指针的值,也不能通过指针改变 value 的值,但是可以直接修改 value 的值。

    三、数组和指针

    • 数组是一个相同类型的对象集合,可以用一个名称引用;
    • 指针是一个变量,它的值是给定类型的另一个变量或常量的地址;
    • 数组和指针关系密切,有时可以互换使用。

    3.1、一维数组

    考虑下面这个例子,这里使用标准函数 scanf():

    char single = 0;
    scanf("%c", &single);
    

    如果需要输入字符串,可以使用以下代码:

    char single[] = "hello c";
    scanf("%s", single);
    

    可以发现这里并没有使用取地址符,而是直接使用数组名,就像使用指针。如果以这种方式使用数组名称使用数组名,而没有带索引值它就引用数组的第一个元素得到地址。

    但是数组和指针之间有一个重要区别:可以改变指针包含的地址,但是不能改变数组名称引用的地址。

    以下例子展示了将一个整数值加到指针(p + 1)产生的效果:

    char exp[] = "this is a example.";
    
    char* p = exp;
    
    for(int i = 0; i < strlen(exp); ++i) {
    
        printf("exp[%d] = %c * (p+%d) = %c   &exp[%d] = %p p+%d = %p\n", i,exp[i], i, *(p+i), i, &exp[i], i, p+i);
    
    }
    

    输出结果如下:

    exp[0] = t * (p+0) = t   &exp[0] = 0x7fff0c3df580 p+0 = 0x7fff0c3df580
    exp[1] = h * (p+1) = h   &exp[1] = 0x7fff0c3df581 p+1 = 0x7fff0c3df581
    exp[2] = i * (p+2) = i   &exp[2] = 0x7fff0c3df582 p+2 = 0x7fff0c3df582
    exp[3] = s * (p+3) = s   &exp[3] = 0x7fff0c3df583 p+3 = 0x7fff0c3df583
    

    可以看出通过 &exp[i] 获取的地址和通过 (p+i) 获取的地址相同,这也是预期的结果。

    3.1 多维数组

    在多维数组中数组名和指针之间的差异更加明显,以一个二维数组为例:

    char board[3][3] = {
                         {'1','2','3'},
                         {'4','5','6'},
                         {'7','8','9'}
                     };
    
    printf("address of board        : %p\n", board);
    printf("address of board[0][0]  : %p\n", &board[0][0]);
    printf("address of board[0]     : %p\n", board[0]);
    

    输出结果:

    address of board        : 0x7fff66b67c2f
    address of board[0][0]  : 0x7fff66b67c2f
    address of board[0]     : 0x7fff66b67c2f
    

    以上三个输出结果相同,由此可以得到推论:声明一维数组时 x[n1] 时,[n1] 放在数组名称之后,告诉编译器这是一个有 n1 个元素的数组。声明二维数组时y[n1][n2]时,编译器就会创建一个大小为 n1 的数组,它的每一个元素是大小为 n2 的数组。

    虽然 board、 board[0] 和 &board[0][0] 的数值相同,但是它们并不是相同的东西:board 是 char 型二维数组的地址,board[0] 是 char 型一维数组的地址,&board[0][0] 是 char 型数组元素的地址。

    用指针记号获取数组中的数值时,必须使用间接运算符,看下面这个例子:

    char board[3][3] = {
                         {'1','2','3'},
                         {'4','5','6'},
                         {'7','8','9'}
                     };
    
    printf("value of board[0][0]  : %c\n", board[0][0]);
    printf("value of *board[0]    : %c\n", *board[0]);
    // board 是 char** 类型,是指针的指针
    printf("value of **board      : %c\n", **board);
    

    输出结果:

    value of board[0][0]  : 1
    value of *board[0]    : 1
    value of **board      : 1
    

    注意:尽管可以把二维数组看成是一维数组的数组,但是在内存中并不是以这种形式存储二维数组,其存储方式为存储一个很长的一维数组,编译器确保可以像一维数组那样访问它。如下所示:

    char board[3][3] = {
                         {'1','2','3'},
                         {'4','5','6'},
                         {'7','8','9'}
                     };
    
    for(int i = 0; i < 9; ++i) {
        // *board 得到二维数组的第一个元素(首个一维数组),*board +i
        // 就是对第一个一维数组进行偏移,在执行解引用就可以得到二维数组
        // 元素值。
        printf(" board: %c\n", *(*board +i));
    }
    

    输出结果:

    board: 1
    board: 2
    board: 3
    board: 4
    board: 5
    board: 6
    board: 7
    board: 8
    board: 9
    

    四、内存的使用

    C 语言中内存划分如下:

    • 栈区:栈内存,存放局部变量,自动分配和释放,里面函数的参数,方法里面的临时变量

    • 堆区:动态内存分配,由程序员手动分配,最大值为操作系统的 80%

    • 全局区或静态区

    • 常量区(字符串)

    • 程序代码区

    C 语言有一个功能:动态分配内存,它依赖指针的概念,为在代码中使用的指针提供了很强的激励机制,它允许在程序执行时动态分配内存。只有使用指针才能动态分配内存。

    4.1、动态分配内存:malloc() 函数

    在运行时分配内存的最简单的标准库函数是 malloc() 函数,使用该函数时,需要在程序中包含头文件 <stdlib.h>。使用该函数时需要指定分配的内存字节数作为参数,该函数返回分配内存的第一个地址,因为返回的是地址,那么就必须使用指针。

    动态分配内存代码如下:

    int* pNumber = (int*)malloc(100); // 这里需要进行类型强转
    

    以上代码可以分配 100 个字节,也就是 25 个 int 值,该语句假定 int 占 4 个字节,但是不同的系统对 int 的大小规定可能不同,因此最好取消这种假设,而使用以下方式分配内存:

    int* pNumber = (int*)malloc(25*sizeof(int));
    

    需要注意的是:

    • malloc() 返回类型为 void*,所以需要进行类型转换。许多编译器会将 malloc 返回的地址自动转化为赋值语句左边的指针类型,但是加上显示的类型转换是无害的;
    • 在 32 位模式中,malloc() 返回的地址总是 8 的倍数;在 64 位模式系统中,该地址总是 16 的倍数;
    • 如果 malloc() 遇到问题,那么它就会返回 NULL,并设置 errno,所以在使用前最好先判断内存是否已经分配;
    • malloc() 不初始化它所分配的内存。

    4.2、释放动态分配的内存

    在使用动态分配内存时,应该总是在不需要该内存时释放它们。释放动态分配的内存必须要能够访问引用内存块的地址,释放内存语句如下:

    free(pNumber);
    pNUmber = NULL;
    

    注意:在释放指针指向的堆内存时,必须确保它不被另一个地址覆盖。

    示例代码,列举指定个数的质数:

    #include <stdio.h>
    #include <stdlib.h>
    #include <stdbool.h>
    
    int main(void) {
    
        unsigned long long *pPrimes = NULL;
        unsigned long long trial = 0;
        bool found = false;
        int total = 0;
        int count = 0;
    
        printf("How many primes would you like - you'll get at least 4? ");
        scanf("%d", &total);
        total = total < 4 ? 4 : total;
    
        pPrimes = (unsigned long long*)malloc(total*sizeof(unsigned long long));
    
        if(!pPrimes) {
            printf("Not enough memory. It's the end I'm afraid.\n");
            return 1;
         }
    
        *pPrimes = 2ULL;        // first prime
        *(pPrimes + 1) = 3ULL;  // second prime
        *(pPrimes + 2) = 5ULL;  // third prime
    
        count = 3;
        trial = 5ULL;
    
        while(count < total) {
            trial += 2ULL;
            for(int i = 1; i < count; ++i) {
                if(!(found = (trial % *(pPrimes + i)))) break;
            }
            if(found) *(pPrimes + count++) = trial;
        }
    
        for(int i = 0; i < total; ++i) {
            printf("%12llu", *(pPrimes + i));
            if(!((i+1) % 5)) printf("\n");
        }
    
        printf("\n");
        free(pPrimes);
        pPrimes = NULL;
        return 0;
    }
    

    4.3、使用 calloc() 函数分配内存

    在头文件 <stdlib.h> 中声明的 calloc() 函数与 malloc() 函数相比有以下几个优点:

    1. 它将内存分配为指定大小的数组;
    2. 它初始化了分配的内存,所有的位均为 0

    calloc() 需要两个参数,数组元素个数和数组元素所占字节数,两个参数类型都是 size_t,函数返回类型为 void*。示例代码如下:

    int* pNumber = (int*)calloc(75, sizeof(int));
    

    如果不能分配,那么函数将返回 NULL,也可以让编译器执行类型转换:

    int* pNumber = calloc(75, sizeof(int));
    

    4.4、扩展动态分配的内存

    realloc() 函数可以重用或扩展之前使用 malloc() 或 calloc() (或 realloc())分配的内存。realloc() 需要两个参数:指针和要分配的新内存字节数。

    参考

    Beginning C

    Pointer on C

    The C Programming

    相关文章

      网友评论

          本文标题:Android NDK 3 C语言数组与指针

          本文链接:https://www.haomeiwen.com/subject/yfevuftx.html