NDK02_C:指针、指针运算、多级指针与函数指针

本文深入探讨了NDK开发中的指针概念,包括指针的基本运算、指针数组、数组指针、多级指针及函数指针的使用。通过实例讲解,帮助读者掌握指针在C语言中的应用技巧。

NDK开发汇总

一 学习指针的目标

  • 从专业角度了解指针的作用
  • 会使用指针进行基本运算
  • 会使用指针作为函数的参数
  • 会使用指针数组和数组指针
  • 会使用二级指针
  • 会使用函数指针

二 指针

指针变量存储的是 变量类型对应的变量的地址。

  • 声明指针或者不再使用后都要将其置为0 (NULL)

  • 野指针 未初始化的指针

  • 悬空指针 指针最初指向的内存已经被释放了的一种指针

  • & 取地址操作符,运用在内存中的对象上面,即变量与数组元素

  • 间接寻址或引用运算符 *

  • &和*都是单目运算符

  • %#x 16进制输出

  • int * p p存储的是int型的变量的地址

double f = 23.99f;
double *fp;
fp = &f;
printf("f的内存:%d\n", sizeof(f));//8
printf("size of(*fp) %d\n", sizeof(*fp));//8
printf("size of(fp) %d\n", sizeof(fp));//4 fp是一个变量,指向double类型的数据的变量;fp是存储的 地址,地址是占用4个字节的。32位 4*8

定义

int *a; 正规
int* a;
int * a;
//因为 其他写法看起来有歧义
int* a,b;

使用:

//声明一个整型变量
int i = 10;
//将i的地址使用取地址符给p指针
int *p = &i;
//输出 0xffff 16进制地址
printf("%#x\n", &i);
printf("%#x\n", &p);

指针多少个字节?指向地址,存放的是地址
地址在 32位中指针占用4字节 64为8

解引用

解析并返回内存地址中保存的值

int i = 10;
int *p = &i;
//解引用  
//p指向一个内存地址,使用*解出这个地址的值 即为 10
int pv = *p;
//修改地址的值,则i值也变成100
//为解引用的结果赋值也就是为指针所指的内存赋值
*p = 100;

三 指针运算:

  • *p 代表是它所指的地址上面的值的运算
  • p++ 是p 指针变量++,结果就是:p指向它的下一个地址
//对指针 进行算数运算
//数组是一块连续内存 分别保存 11-55
//*p1 指向第一个数据 11,移动指针就指向第二个了
int i1[] = {11,22,33,44,55};
int *p1 = i1;
for (size_t i = 0; i < 5; i++)
{
    //自增++ 运算符比 解引用* 高,但++在后为先用后加
    //如果++在前则输出 22-55+xx
	printf("%d\n", *p1++);
    //p1[0] == *(p1+1) == s[1]

}

指针指向地址,指针运算实际上就是移动指针指向的地址位置,移动的位数取决于指针类型(int就是32位)

四 数组与指针

通过数组下标所能完成的任何操作都可以通过指针来实现。而用指针编写的程序比用数组下标编写的程序执行速度快,但是,指针写的程序会比较难理解一点。

在c语言中,指针和数组名都表示地址

1、数组是一块内存连续的数据。

2、指针是一个指向内存空间的变量

int i1[] = {11,22,33,44,55};
//直接输出数组名会得到数组首元素的地址
printf("%#x\n",i1);
//解引用
printf("%d\n",*i1);
//将数组名赋值给一个指针,这时候指针指向数组首元素地址
int *p1 = i1;

五 指针数组和数组指针

指针数组

  • 是一个数组,数组里存储的是指针类型的变量, 如: int *p1[4];
//二维数组类型是 int (*p)[x]
int array[2][3] = { {11,22,33},{44,55,66} };
//也可以 int array[2][3] = {  11,22,33 ,44,55,66 };
//array1 就是一个 int[3] 类型的指针
int (*array1)[3] = array;
//怎么取 55 ?
//通过下标
array[1][1] == array1[1][1]
//通过解引用
int i = *(*(array1 + 1) + 1);
//拆分
//1、 指针偏移 因为array1的类型是3个int的数组 所以 +1 移动了12位
array1 + 1 
//2、获得{44,55,66}数组  (*(array1 + 1))[1] = 55
(*(array1 + 1) 
//3、对数组执行 +/- 相当于隐式的转为指针
//获得 55 的地址 再解地址
*(array1 + 1) + 1

数组指针

  • 是一个指针,这个指针指向的是一个数组的首地址,如:int (*p2)[4]
int *array2[2];
array2[0] = &i;
array2[1] = &j;

六 多级指针

  • 指向指针的指针

二级指针存的内容是一个一级指针的地址
// p 是一个指针变量,这个变量本身就是一个地址的别名
// *p 运算,得到的就是a 的值
// *p2 运算,得到的就是p的值
//**p2 运算,得到的就是 p的值的 *运算得到的值 a;

一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。

int a = 10;
int *i = &a;
int **j = &i;
// *j 解出 i   
printf("%d\n", **j);

七 函数指针

函数指针是指向函数的指针变量

void println(char *buffer) {
	printf("%s\n", buffer);
}
//接受一个函数作为参数
void say(void(*p)(char*), char *buffer) {
	p(buffer);
}
void(*p)(char*) = println;
p("hello");
//传递参数
say(println, "hello");

//typedef 创建别名 由编译器执行解释
//typedef unsigned char u_char;
typedef void(*Fun)(char *);
Fun fun = println;
fun("hello");
say(fun, "hello");

//类似java的回调函数
typedef void(*Callback)(int);

void test(Callback callback) {
	callback("成功");
	callback("失败");
}
void callback(char *msg) {
	printf("%s\n", msg);
}

test(callback);
<think> 我们正在分析一个崩溃问题,崩溃发生在`std::__ndk1::__hash_table`的`rehash`函数中,同时涉及`std::atomic<bool>`的使用(`Fork::support_enabled_`的加载)和`std::shared_ptr`的构造(`make_shared`)。我们首先需要理解这些组件在崩溃时可能如何交互。 首先,我们注意到崩溃的堆栈涉及两个主要部分: 1. `Fork::support_enabled_.load(std::memory_order_relaxed)` 2. `std::__ndk1::__hash_table`的`rehash`操作(位于`__hash_table`文件的2375行) 3. `std::shared_ptr`的`make_shared`操作(用于`aqua::app_communication::GrpcServerSignal`) 我们已知的信息: - `Fork::support_enabled_`是一个原子布尔变量,初始为`false`。使用`memory_order_relaxed`加载,这意味着它只保证原子性,不提供同步或顺序保证。 - `__hash_table`的`rehash`操作:当哈希表需要扩容时会重新哈希所有元素,这通常是一个耗时的操作,并且需要保证线程安全。 - `shared_ptr`的`make_shared`:动态分配对象并构造一个共享指针。 崩溃可能发生在多线程环境中,其中一个线程正在修改哈希表(例如插入元素导致rehash),而另一个线程同时访问哈希表或进行其他操作。 我们考虑以下可能的原因: 1. **原子变量使用`memory_order_relaxed`导致的问题**: - `support_enabled_.load(std::memory_order_relaxed)`用于检查是否启用了fork支持。由于使用relaxed内存序,它不会其他内存操作同步。如果另一个线程在修改这个标志(例如设置为`true`)的同时,有线程在读取,虽然读取的原子性保证了不会读到中间值(只能是`true`或`false`),但是读取线程可能看不到最新的值(尽管对于布尔值,这通常不是问题,因为只有两个值)。然而,这里更大的问题是,这个标志的加载可能用于控制某些fork相关的行为,而在fork过程中操作多线程数据结构(如哈希表)是危险的。 2. **fork多线程的交互问题**: - 在支持fork的系统中,如果程序在多个线程运行时调用fork,那么新进程中只有调用fork的线程被复制,其他线程都会消失。但是,在fork之前,其他线程可能正持有锁(如哈希表内部的锁)或处于不一致状态(如正在rehash),这将导致子进程中的数据结构处于损坏状态。 - 如果`Fork::support_enabled_`被设置为`true`,那么程序可能在某个时刻准备fork。但即使没有fork发生,如果哈希表的操作没有考虑到fork安全,也可能出现问题。 3. **哈希表的rehash操作不是线程安全的**: - 虽然标准库的容器通常不是线程安全的,但这里我们注意到崩溃发生在`rehash`函数中。如果多个线程同时操作同一个哈希表(例如一个线程在插入元素触发rehash,另一个线程也在插入或读取),那么就会导致数据竞争,进而崩溃。 4. **shared_ptr的构造哈希表操作的竞争**: - 堆栈中同时出现了`shared_ptr`的构造和哈希表的rehash。可能的情况是,在哈希表中存储的是`shared_ptr`,当插入一个新的`shared_ptr`时,触发了rehash。而`make_shared`会分配内存并构造对象,这期间如果另一个线程正在rehash,并且哈希表没有适当的同步,就会导致问题。 5. **Android NDK环境下的特殊问题**: - 堆栈路径指向了NDK中的头文件,表明这是在Android平台上运行的。NDK的C++标准库实现(libc++)可能有特定的行为。 结合堆栈信息,我们特别关注`__hash_table:2375`行(在rehash函数中)的崩溃。我们可以查找libc++的源代码来了解该行代码在做什么。 假设我们无法直接获取NDK中该行代码,但通常rehash函数会重新分配桶数组,并将所有元素重新哈希到新桶中。在这个过程中,如果其他线程同时修改哈希表(例如插入、删除),那么就会导致竞争条件。 另外,我们注意到`Fork::support_enabled_`的加载出现在崩溃堆栈中,说明在崩溃发生前,程序检查了这个标志。这可能是崩溃发生的前置条件,或者崩溃有间接关系。 一个合理的推测是:程序在某个时刻启用了fork支持(将`support_enabled_`设为true),然后在一个多线程环境中,某个线程调用了fork,而此同时,另一个线程正在执行哈希表的rehash操作。在子进程中,rehash操作可能因为锁被永久占用(因为持有锁的线程在子进程中不存在)或数据结构处于不一致状态而导致后续操作崩溃。 但是,我们并没有在堆栈中看到fork调用,所以也可能是主进程中的问题。另一种可能是:即使没有fork,由于`support_enabled_`被设置为true,程序可能安装了一些fork处理程序(例如通过`pthread_atfork`),这些处理程序在fork发生时会被调用。如果这些处理程序哈希表的操作存在竞争,也可能导致问题。 然而,我们更倾向于认为崩溃是由于多线程同时访问哈希表导致的。因为`rehash`操作通常不是线程安全的,而我们在堆栈中看到`rehash`,说明它正在执行中,此时如果有另一个线程也操作哈希表,就会崩溃。 那么为什么堆栈中会有`Fork::support_enabled_.load`?这可能是因为在哈希表的操作过程中(比如在插入元素时),会检查这个标志以确定是否进行某些fork相关的操作(如加锁)。如果我们查看libc++的源代码,我们可能会发现,在rehash过程中会检查一些全局设置,其中可能包括这个fork支持标志。 但是,根据提供的代码片段,我们没有看到哈希表操作和`Fork`类之间的直接联系。因此,我们需要更多的上下文。 考虑到崩溃发生在`__hash_table:2375`行,我们可以假设该行代码类似于: ```cpp // 在rehash函数中 for (size_type __i = 0; __i < __bc; ++__i) { // ... 重新哈希每个桶的元素 } ``` 或者是对某个迭代器的操作。常见的崩溃原因可能是: - 迭代器失效(在rehash过程中,其他线程修改了桶) - 访问了无效的内存(如已经被释放的节点) 由于哈希表中存储的是`shared_ptr`,我们还需要考虑引用计数的原子操作。`shared_ptr`的引用计数是原子操作,通常不会引起问题,但是如果在rehash过程中,某个`shared_ptr`被另一个线程重置,那么当rehash访问该节点时,可能节点已经被释放(如果引用计数降为0)。 但是,在rehash过程中,哈希表会持有所有元素的引用(即增加引用计数)吗?通常不会。rehash操作只是重新组织表结构,并不会改变元素的所有权。所以,如果另一个线程删除了一个元素(导致其引用计数降为0),那么rehash过程中访问这个元素就会导致野指针。 因此,根本原因可能是:这个哈希表不是线程安全的,而多个线程在没有同步的情况下同时访问它(一个线程rehash,另一个线程删除元素)。 解决方案: 1. 确保对哈希表的操作(插入、删除、查找)都是互斥的,例如使用互斥锁。 2. 检查`Fork::support_enabled_`的使用:如果它确实fork相关,确保在fork时所有线程都处于安全状态(例如,在fork前停止所有其他线程,或使用`pthread_atfork`注册处理程序来加锁)。 但是,为什么`Fork::support_enabled_`的加载会出现在堆栈中?可能是在每次操作哈希表之前,程序会检查这个标志以决定是否进行额外的同步(例如,如果启用了fork支持,则使用锁)。如果这个检查是使用relaxed序,那么它可能没有正确同步。 例如: ```cpp if (Fork::support_enabled_.load(std::memory_order_relaxed)) { // 使用锁或其他同步机制 } ``` 但是,由于是relaxed序,可能在标志被设置为true之后,这个线程并没有看到,因此跳过了同步,而此时另一个线程正在fork,导致问题。 然而,这个标志通常只会在程序初始化时设置一次(从false变为true),之后不再修改。所以,使用relaxed序读取是安全的(因为一旦看到true,就永远为true)。但是,在设置之前和设置之后,线程需要正确同步以看到新的值。由于设置操作(store)可能使用更严格的内存序(如release),而读取使用acquire(但这里使用了relaxed),所以可能有些线程在设置后很长时间内仍然看到false。不过,这通常不会导致崩溃,只是导致没有使用fork支持的同步机制。 所以,更可能的原因还是哈希表本身的线程不安全。 综上所述,崩溃原因可能是多线程竞争访问哈希表(一个线程触发rehash,另一个线程同时修改哈希表)。而`Fork::support_enabled_`的加载只是崩溃路径上的一个操作,可能不是根本原因。 建议: 1. 检查代码,确保所有对哈希表的操作都进行了适当的同步(如使用互斥锁)。 2. 如果确实需要支持fork,请确保在fork时没有线程正在操作哈希表(通过`pthread_atfork`在fork前加锁,fork后解锁)。 由于我们没有完整的代码和堆栈,以上分析基于典型情况。 注意:在Android NDK的libc++中,`__hash_table`的实现可能包含一些调试或特定于平台的代码,所以崩溃的具体位置(2375行)需要结合源码分析。但无论如何,多线程竞争是常见问题。 因此,我们得出结论:崩溃的根本原因可能是哈希表在多线程环境下未加锁操作导致的竞争条件。需要给哈希表加上锁。 </think>### 崩溃分析:原子变量哈希表rehash的并发冲突 #### 1. **崩溃场景还原** ```mermaid sequenceDiagram Thread A->>+HashTable: 执行rehash操作 Thread B->>+AtomicVar: 读取support_enabled_(relaxed序) Thread B->>+HashTable: 插入新元素(触发shared_ptr构造) Thread A-->>-HashTable: 正在rehash时内存结构变化 Thread B--xHashTable: 访问失效内存导致崩溃 ``` #### 2. **核心崩溃原因** 1. **内存序使用不当** `support_enabled_.load(std::memory_order_relaxed)` 的宽松内存序无法哈希表操作建立同步关系: $$ \text{同步缺失} \implies \text{数据竞争} $$ - relaxed序不保证操作顺序,导致哈希表状态可见性无法保障 - 当rehash修改桶数组时,其他线程可能看到中间状态 2. **rehash并发问题** 哈希表rehash过程涉及关键操作: ```cpp // __hash_table:2375 附近典型实现 void rehash(size_type __n) { __pointer_allocator& __npa = __bucket_list_.get_allocator(); __bucket_list_.reset(); // 释放旧桶数组 __create_buckets(__n); // 分配新桶数组 // ... 元素迁移 ... } ``` - 旧桶数组释放后,其他线程访问会导致野指针 - 新桶数组未完成初始化时访问引发内存错误 3. **shared_ptr构造竞争** `make_shared`在构造对象时: ```cpp std::__ndk1::shared_ptr<GrpcServerSignal>::make_shared() ``` - 涉及内存分配和引用计数初始化 - 若rehash并发执行,可能访问已释放的哈希表节点 #### 3. **根本原因定位** | 因素 | 影响 | 解决方案方向 | |------|------|------------| | 宽松内存序 | 丧失同步保障 | 升级内存序 | | 非原子rehash | 结构非一致性 | 增加锁机制 | | shared_ptr构造 | 内存分配竞争 | 分离资源管理 | #### 4. **修复方案** 1. **内存序强化** 修改原子标志加载方式: ```cpp // 原代码 (不安全) support_enabled_.load(std::memory_order_relaxed); // 修改后 (建立同步) if(support_enabled_.load(std::memory_order_acquire)) { // 受保护操作 } ``` - `acquire`序可能的`release`存储配对 - 建立 happens-before 关系:$ \text{store-release} \rightarrow \text{load-acquire} $ 2. **rehash并发控制** 添加细粒度锁: ```cpp void rehash(size_type __n) { std::lock_guard<std::mutex> lock(rehash_mutex_); // ... 原有rehash逻辑 ... } ``` - 确保桶数组重置和重建操作的原子性 - 插入/删除操作共用同一锁 3. **shared_ptr构造隔离** 预分配策略: ```cpp // 在rehash前完成对象构造 auto new_obj = std::make_shared<GrpcServerSignal>(); { std::lock_guard lock(table_mutex); table.insert({key, new_obj}); } ``` #### 5. **崩溃路径验证** $$ \text{并发rehash} + \text{relaxed序访问} \rightarrow \text{访问失效桶指针} \rightarrow \text{SIGSEGV} $$ 典型崩溃栈: ``` #0 0x0000 in __hash_table::rehash() at line 2375 #1 0x0000 in Fork::check_support() (调用support_enabled_.load(relaxed)) #2 0x0000 in shared_ptr<GrpcServerSignal>::make_shared() #3 0x0000 in 业务逻辑插入操作 ``` 证明原子变量访问触发了哈希表的不安全操作[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值