NDK 之 C 语言 入门与指针

在 java 语言中,万物皆对象

在 Linux 中,万物皆文件

而在 C 和 C++ 中,万物皆置针

C 语言

C 语言为面向过程的语言,在函数加载的时候,main 函数作为程序入口,应在文件的最后进行声明

C 语言不允许函数重载,java 和 C++可以

.h 文件为声明文件;.c 文件为实现文件(在c++ 中,为.cpp)

#include 引入其他文件,其中,尖括号(<>)引入的,是 C 语言库中的模块, 双引号("")引入的,是用户实现的声明文件

基础类型:int、long、float、double、char 等,值得注意的是,没有string,只有char[]

对应的占位符:%d、%ld、%f、%lf,%c 等,可以直接打印 char[],其占位符为%s;地址占位符为 %p

// C 语言中,没有所谓的 string 类型,字符串的定义用的是 char[]
char[] str = "abc"

指针

定义变量时,会给变量开辟一个内存空间地址。

指针是一类数据类型,具体对应的是数据的内存地址

// 定义了一个 int 类型的变量 a 
// 将 a 赋值为 1
int a = 1;
// 定义了一个 int * 类型的变量 ap 
// 将 ap 赋值为 变量 a 的存储地址
int *ap = &a;

// 定义了一个 int 类型变量 b,开辟了新的空间地址
// 将变量 a 值,赋值给 b
int b = *ap;
// 定义了一个 int 类型变量 c,开辟了新的空间地址
// 将地址 ap 中储存的值,赋值给 c
int c = a;

// 将 ap 地址对应的位置,修改为 2,即 a 修改为 2
// 而 b 和 c 的地址是新开辟的,所以修改 ap 地址对应位置,并不会修改 b 和 c 的值
*ap = 2;

其在内存映射中的图示大致为:


pointers.001.jpeg

在函数变量传参时,会重新定义一次入参,所以如果需要通过函数修改变量的值,需要给函数传入对应的变量地址,而不是直接传入一个变量

void change(int c, int d) {
    int temp = c;
    c = d;
    d = temp;
}
int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;
    change(a, b);
    return 0;
}

相当于:

int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;
  
    int c = a;
    int d = b;
    // 修改的是新开辟出存储空间的 c 和 d 的值,并不会影响到 a 或者 b
    int temp = c;
    c = d;
    d = temp;
    // 运行结果为:a: 1, b: 2, c: 2, d: 1 
    // printf("a: %d, b: %d, c: %d, d: %d \n", a, b, c, d);
    return 0;
}

采用指针作为函数参数:

void change2(int *c, int *d) {
    int temp = *c;
    *c = *d;
    *d = temp;
}

int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;
    change2(&a, &b);   
    return 0;
}

相当于:

int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;

    // 将 a 的地址赋值给 c
    int *c = &a;
    int *d = &b;
    // 将 c 地址存储的数据取出,并赋值给 temp
    int temp = *c;
    // 修改 c 地址存储的值,即修改了 a 的值
    *c = *d;
    *d = temp;
    // 运行结果: a: 2, b: 1, c: 2, d: 1
    // printf("a: %d, b: %d, *c: %d, *d: %d \n", a, b, *c, *d);
    return 0;
}

多级指针

当一个指针变量指向另一个指针变量时,例如在上述的例子中,int 类型指针变量(int *) ap 指向了 a;新建另一个新的int * 类型指针变量(int **) app,指向指针变量 ap,此时 app 则被称为二级指针。实际开发中,一般不会有超过三级指针的多级指针。

// 新建 int 类型变量 a,赋值为 0
int a = 0;
// 新建 int * 类型变量 ap,赋值为 a 的地址
int *ap = &a;
// 新建 int ** 类型变量 app,赋值为 ap 的地址
int **app = &ap;
// 新建 int *** 类型变量 appp,赋值为 app 的地址
int ***appp = &app;

// 不能用以下代码对多级指针赋值
// 因为指针是指向具体内存地址的变量,在 &(&(&a)) 中,只有 a 有具体内存地址值,&a 操作可成立
// 但 &a 操作的值没有另外开辟内存空间进行存储,所以无法计算 &(&a)
// int ***appp = &(&(&a));

printf("a: %d, ap: %p, app: %p, appp:%p\n", a, ap, app, appp);
printf("&a: %p, &ap: %p, &app: %p, &appp: %p\n", &a, &ap, &app, &appp);
printf("a: %d, *ap: %d, *app: %p, *appp: %p", a, *ap, *app, *app);

在指针变量的定义时,类型变量后面添加了多少个 *,即代表指针的层级,指针的层级决定这个指针需要进行几次取值操作后,方能获取最开始存储的数据。其内存示意图,大致如下:


pointers.002.jpeg

& 操作 -> 获取变量的地址

* 操作 -> 对指针变量的值进行寻址,并获取该地址上的内容

注意: * 操作可以叠加,而 & 操作不可以

数组指针

定义数组时,C 与 java 不同,[] 写在变量名后,而不是类型后;此外,无法在定义时,将一个数组的内容赋值给另一个数组。数组的拷贝需要用到 memcpy 函数

数组在内存空间内的存储是连续的,因而 a[0] 的位置就是数组的起始位置。
当输出 a 的值时,输出的,其实是 a 的地址。可以理解为在 java 中,直接输出一个对象时,输出的其实也是 hash 值(内存映射值)。
因而:a、&a、&a[0],都是数组的起始地址位置,输出的值相同

在 C 中,没有直接获取数组长度的方法,需要通过 数组的内存大小 除以 数组元素类型的大小 来进行计算,并获得结果

数组 a 作为指针时,可以进行位移运算,但无法进行自增自减操作。(可以将 a 视为 finnal 变量)

可以定义一个专门的指针 (aar)对新定义的指针进行增减操作;

指针加减时,指针每 + 1,输出的指向地址 + 类型占位字节数(char -> 1, int -> 4, long ->8)

在 C 语言中,不一定存在数组越界而崩溃的情况。
根据不同的操作系统,不同的编译器,可能会有不同的表现。
有些可以读取出一个野值(即未知意义的一个内存地址值,这也是为什么有些时候 C 代码很难排查 bug,取出了一个不正确的数值,但没有任何报错,只是最终运行的结果与期望值不同);mac 系统上,运行时会直接抛出异常。

// 定义数组 a
int a[] = {1,2,3,4,1000};
// 输出的三个值相同
printf("a: %p, &a: %p, &a[0]: %p\n", a, &a, &a[0]);

// 计算数组的长度
int aSize = sizeof(a) / sizeof(int);

// 拷贝数组时,以下代码不合法:
// int b[] = a;
//  需使用以下方法:
// 其中,数组的大小,在定义时就必须明确,且其值为元素个数
int b[aSize];
// 拷贝时,将 a 数组的 sizeof(a) 字节拷贝到 b 数组对应的地址中
memcpy(b, a, sizeof(*a));

printf("sizeof(a): %lu, sizeof(int): %lu, sizeof(a)/sizeof(int): %lu\n", sizeof(a), sizeof(int), aSize);

//定义一个指针,指向 a 的起始位置
int *aar = a; // a == &a == &a[0]
for (int i = 0; i < aSize; i ++) {
    // a 可以进行运算,得出遍历的下一个元素值
    // 输出 a + i 时,可发现,a 每 + 1,地址 + 4。因为 int 占位 4字节
    printf("*(a + i): %d, a + i: %p\n", *(a + i), a + i);

    // 需要对地址进行位移运算后,再进行指针取值运算,需要将 a + i 用括号括起
    // 如果没有括起的话,则是取出了第一个元素的值,进行了值的加运算后再输出
    // 在 1,2,3,4,5 这样的数组中,输出一致,但意义不对,是 bug
    // printf("test: %d\n", *a + i);

    // 可以通过 a[i] 的方式,取出对应位置元素值
    printf("a[i]: %d\n", a[i]);

    // 也可以通过取出自增指针对应位置的元素值,来遍历数组
    printf("aar: %p\n", aar ++);
}
// 以下代码,在 xcode 中运行时报错
// printf("a[1000]: %d", a[1000]);
// a[1000] = 20;

函数指针

C 语言中,函数在传参时,会给基础类型开辟新的存储空间,而复杂的结构体,则会传递结构体指针。
数组属于结构体,传递的是指针。
即便参数用的是 int a[] 类型,实际传递的也是 int *a。
int size = sizeof(a) / sizeof(*a); 在定义 a 数组的结构体中,得到的值是数组的长度, sizeof(a) 能成功获取到 a 数组的字节数;而在参数为 int a[] 的函数体内,得到的值为 1,此时 sizeof(a) 相当于 sizeof(*a) ,因为在函数参数中, int a[] 的实质为 int *a(int 指针)类型的变量。

函数也是一种复杂结构,因而直接使用函数名 和 直接使用数组名 一样,获取到的,都是对应的指针位置。

函数指针作为 函数参数时的定义结构:{函数参数返回值类型} (*{函数体中使用的函数参数别名})({函数参数的入参类型,用逗号隔开})

// 第一种打印
// 等同于 void printArray1(int *a, int aSize, char *name)
void printArray1(int a[], int aSize, char *name) {
    int *aar = a;
    // 在函数体内,无法获取数组类型参数的长度,因为此参数的实质是指针
    // int size = sizeof(a) / sizeof(*a);
    int i;
    for (i = 0; i < aSize; i ++) {
        printf("%sar: %d\n", name, *(aar ++));
        // 在此修改数组内元素的值。此处修改,会改变原来数组内的值。即其他方法打印与此方法一致。
        // 从第二个元素开始,将值改为第几个元素
        * aar = i + 2;
    }
}

// 第二种打印
void printArray2(int *a, int aSize, char *name) {
    int i;
    for (i = 0; i < aSize; i ++) {
        printf("%s[i]: %d\n", name, a[i]);
    }
}

// 必须在 main 之前先声明函数,但实现可以在 main 之后进行
// 第三种打印
void printArray3(int *a, int aSize, char *name);

// 打印数组的函数,可以将具体函数作为参数传递
// void (*method)(int[], int, char *) 为第一个参数,类型为:函数地址,其中:
// 函数参数返回值类型:void;函数体中使用的函数参数别名:method;
// 函数参数的入参类型,用逗号隔开:int[], int, char *
void print(void (*method)(int[], int, char *), int a[], int aSize, char * name) {
    // 取出 method 地址对应的函数,并调用
    (*method)(a, aSize, name);
    // 由于在参数中已声明 method 是一个函数指针,因而 * 可省略,等价于:
    // method(a, aSize, name);
}

int main(int argc, const char * argv[]) {
    int a[] = {7,2,3,4,1000};
    int size = sizeof(a) / sizeof(*a);
    // 采用不同的打印方法打印数组
    print(printArray1, a, size, "a");
    print(printArray2, a, size, "a");
    print(printArray3, a, size, "a");
    return 0;
}

// 在 main 函数之后实现之前声明的函数
void printArray3(int *a, int aSize, char *name) {
    int i;
    for (i = 0; i < aSize; i ++) {
        printf("*(%s + i): %d\n", name, *(a + i));
    }
}

C语言内存地址划分

指针是内存地址的映射,因而,在进一步了解指针前,先对 C 语言的内存划分有个大致概念:

常量(1、“ABC”)、static 修饰的静态变量,会在 常量区 进行存储,由操作系统在程序的运行结束的时候进行销毁

当函数运行时,通过int a = 1;char str[] = "abc"静态开辟 的临时变量会在 栈区 开辟存储空间;当函数运行结束后,对应的栈区变量会被销毁。

通过 malloc 动态开辟 的变量属于 堆区 变量、需要手动调用 free进行销毁

当创建变量时,内存分配如下图所示:

pointers.003.jpeg

Note:字符串类型常数,在存储时,会自动以 '\0' 结尾,表示字符数组结束

C 语言内存四区

  • 全局区

    该区域由操作系统管理,在程序结束后,由操作系统进行释放。

    全局区存放了:在函数外部定义的全局变量(全局变量区)、static 修饰定义的全局静态变量和局部静态变量(静态变量区) 以及 由const修饰的全局变量和字符常量(常量区)

    常量区内存放的数据不可更改,const修饰的局部变量可通过指针更改,但它是局部变量,不在全局常量区内。

// 以下代码中,常量 "abc" 和 常量 "abcdef" 在所有函数里面打印出来的地址都一致
char *getString1() {
    char *test1 = "abcdef";
    printf("string1 abc: %p, test1: %p\n", &"abc", test1);
    return test1;
}

char *getString2() {
    char *test2 = "abcdef";
    printf("string2 abc: %p, test2: %p\n", &"abc", test2);
    return test2;
}


int main(int argc, const char * argv[]) {
    char *test1 = getString1();
    char *test2 = getString2();
    printf("test1 in main: %p, test2 in main: %p\n", test1, test2);
    char *test3 = "abcdef";
    printf("string3 abc: %p, test3: %p\n", &"abc", test3);
    return 0;
}
  • 栈区

    定义在函数内部的 静态开辟 的变量都存放在栈区

    静态开辟操作:int a、float b、char c、char string[] 等 直接定义一个类型变量

    栈区内空间大小有限,不同的操作系统平台限制不同,不提倡在栈内开辟超过1M的变量空间

    栈区的变量,在函数入栈(开始执行)时创建;在函数出栈(执行完毕)后自动释放

char *getString1() {
    // char test1[] 会在栈区开辟一块内存地址,并赋值为 "abc"
    // 而 char *test1 = "abc" 则会直接取 "abc" 常量的地址
    char test1[] = "abc";
    // 此处打印正常,test1 的值为 "abc",地址由系统分配
    printf("test1 in getString1: %s, address: %p\n", test1, test1);
    return test1;
}

int main(int argc, const char * argv[]) {
    // getString1 的函数出栈后,函数内的局部变量会被释放
    // 因而此处 test1 的地址与 getString1 内打印地址相同
    // 但取出来的字符为乱码(空间被释放)
    char *test1 = getString1();
    printf("test1 in main: %s, address: %p\n", test1, test1);
    return 0;
}
  • 堆区

    堆区由程序员手动申请(malloc)和释放(free),称为动态开辟

    只申请不释放会造成内存泄漏。

    释放后,需要将对应指针置空,否则会出现悬空指针。

    空间释放只进行一次,否则可能会崩溃(不同编译平台和操作系统平台可能会有不同处理)

    在 mac 中,栈区的空间被释放后再进行读取,会出现乱码;而堆区的空间被释放后进行读取,不会乱码;但如果被释放的空间被重新分配过,会读取到错误数据。

    可以使用 realloc 对原本申请了的区域进行扩充,如果紧接着的内存区域有足够的空闲,会直接在原有的地址延伸空间大小;如果没有足够的空闲,会重新开辟一个足够大的地址,把原先的数据拷贝到新的地址,并释放原先地址,返回新的地址。

char *getString1(const char* string, int index) {
    // 实际开发中不要这么分配空间大小,可能不足,也可能盈余
    char *test1 = (char *) malloc(20);
    if (test1) {
        // 拼接字符串,填充申请的空间
        sprintf(test1, "%s%d", string , index);
    }
    return test1;
}

int main(int argc, const char * argv[]) {
    int i = 1;
    const char *string = "string";
    char *p = getString1(string, i);
    if (p) {
        printf("%d, get string from heap: %s, address %p\n", i, p, p);
        // 错误的释放内存方式:
        free(p);
        // 在 free 之后,紧接着,应该把相关的指针置为 NULL,否则出现悬空指针
        // 悬空指针可能导致未知错误
        // p = NULL;
        // 重新申请一次堆内存,因为 p 已经被释放,所以这里重新申请的内存,很可能与 p 的内存一致
        char *p2 = getString1(string, i + 1);
        // 如果 p2 与 p 的内存地址一致,则此处 p 取出了一个异常值 string2(应为string1)
        printf("%d, p1 from heap after free: %s, address: %p\n", i, p, p);
        printf("%d, p2 from heap after free: %s, address: %p\n", i, p2, p2);
        // 释放前,先对指针进行判空处理,防止二次释放
        if (p2) {
            // free(pointer) 之后,紧接着将 pointer = NULL,防止后续再次使用悬空指针 和 二次释放
            free(p2);
            p2 = NULL;
        }
    }
    return 0;
}
  • 代码区(程序区)

    存放代码,将函数题转化为二进制数存放进该区域

C 语言函数的压栈和出栈

C 是面向过程的语言,在程序执行的的时候,从 main 函数开始,将 main 函数压入执行栈,然后调用哪个函数,就将该函数压入栈中,执行完毕后将该函数从栈中移除,并且销毁该函数创建的所有临时变量。

void getStringSize(char *str, int *sp) {
    char *cur = str;
    *sp = 0;
    while (*cur) {
        cur ++;
        (*sp) ++;
    }
}

int main(int argc, const char * argv[]) {
    char a[] = "abc";
    int size = 0;
    getStringSize(a, &size);
    printf("size of string a is %d\n", size);
    return 0;
}
pointers.004.jpeg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容