C进阶指南(2):数组和指针、打桩

2024-04-20 12:32

本文主要是介绍C进阶指南(2):数组和指针、打桩,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原文地址:http://pfacka.binaryparadise.com/articles/guide-to-advanced-programming-in-C.html

翻译地址:http://blog.jobbole.com/72830/

三、指针和数组

尽管在某些上下文中数组和指针可相互替换,但在编译器看来二者完全不同,并且在运行时所表达的含义也不同。

当我们说对象或表达式有类型的时候,我们通常想的是定位器值的类型,也叫做左值。当左值有完全non-const类型时,此类型不是数组类型(因为数组本质是内存的一部分,是个只读常量,译者注),我们称此左值为可修改左值,并且此变量是个值,当表达式放到赋值运算符左边的时候,它被赋值。若表达式在赋值运算符的右边,此变量不必被修改,变量成为了修改左值的的内容。若表达式有数组类型,则此表达式的值是个指向数组第一个元素的指针。

上文描述了大多数场景下数组如何转为指针。在两种情形下,数组的值类型不被转换:当用在一元运算符 &(取地址)或 sizeof 时。参见C99/C11标准 6.3.2.1小节:

除非它是sizeof或一元&运算符,再或者它是用于初始化数组的字符文本,否则有着“数组”的表达式被转换为“指向”的类型的指针,此指针指向数组对象的首个非左值元素。

由于数组没有可修改的左值,并且在绝大多数情况下,数组类型的表达式的值被转为指针,因此不可能用赋值运算符给数组变量赋值(即int a[10]; a = 1;是错的,译者注)。下面是一个小示例:

short a[] = {1,2,3};
short *pa;
short (*px)[];void init(){pa = a;px = &a;printf("a:%p; pa:%p; px:%p\n", a, pa, px);printf("a[1]:%i; pa[1]:%i (*px)[1]:%i\n", a[1], pa[1], (*px)[1]);
}


(译者注:%i能识别输入的八进制和十六进制)

a 是 int 型数组,pa 是指向 int 的指针,px 是个未完成的、指向数组的指针。a 赋值给 pa 前,它的值被转为一个指向数组开头的指针。右值表达式 &a 并非意味着指向 int,而是一个指针,指向int 型数组因为当使用一元符号&时右值不被转换为指针。

表达式 a[1] 中下标的使用等价于 *(a+1),且服从如同 pa[1] 的指针算术规则。但二者有一个重要区别。对于 a 是数组的情况,a 变量的实际内存地址用于获取指向第一个元素的指针。当对于 pa 是指针的情况,pa 的实际值并不用于定位。编译器必须注意到 a 和 pa见的类型区别,因此声明外部变量时,指明正确的类型很重要。

int a[];
int *pa;


但在另外的编译单元使用下述声明是不正确的,将毁坏代码:

extern int *a;
extern int pa[];


3.1 数组作为函数形数

某些类型数组变为指针的另一个场合在函数声明中。下述三个函数声明是等价的:

void sum(int data[10]) {}void sum(int data[]) {}void sum(int *data) {}


编译器应报告函数 sum 重定义相关错误,因为在编译器看来上述三个例子中的参数都是 int 型的。.

多维数组是有点棘手的话题。首先,虽然用了“多维”这个词,C并不完全支持多维数组。数组的数组可能是更准确的描述。

typedef int[4] vector;
vector m[2] = {{1,2,3,4}, {4,5,6,7}};
int n[2][4] = {{1,2,3,4}, {4,5,6,7}};


变量 m 是长度为2的 vector 类型,vector 是长为4的 int 型数组。除了存储的内存位置不同外,数组 n 与 m 是相同的。从内存的角度讲,两个数组都如同括号内展示的内容那样,排布在连续的内存区域。访问到的和声明的完全一致。

int *p = n[1];
int y = p[2];


通过使用下标符号 n[1],我们获取到了每个元素大小为4字节的整型数组。因为我们要定位数组的第二个元素, 其位置在多维数组中是数组开始偏移四倍的整型大小。我们知道,在这个表达式中整型数组被转为指向 int 的指针,然后存为 p。然后 p[2] 将访问之前表达式产生的数组中的第三个元素。上面代码中的 y 等价于下面代码中的 z:

int z = *(*(n+1)+2);


也等价于我们初学C时写的表达式:

int x = n[1][2];


当把上文中的二维数组作为参数传输时,第一“维”数组会转为指针,指向再次阵列的数组的第一个元素。因此不需要指明第一维。剩余的维度需要明确指出其长度。否则下标将不能正确工作。当我们能够随心所欲地使用下述表格中的任一形式来定义函数接受数组时,我们总是被强制显式地定义最里面的(即维度最低的)数组的维度。

void sum(int data[2][4]) {}void sum(int data[][4]) {}void sum(int (*data)[4]) {}


为绕过这一限制,可以转换数组为指针,然后计算所需元素的偏移。

void list(int *arr, int max_i, int max_j){int i,j;for(i=0; i<max_i; i++){for(j=0; j<max_j; j++){int x = arr[max_i*i+j];printf("%i, ", x);}printf("\n");}
}


另一种方法是main函数用以传输参数列表的方式。main函数接收二级指针而非二维数组。这种方法的缺陷是,必须建立不同的数据,或者转换为二级指针的形式。不过,好在它运行我们像以前一样使用下标符号,因为我们现在有了每个子数组的首地址。

int main(int argc, char **argv){int arr1[4] = {1,2,3,4};int arr2[4] = {5,6,7,8};int *arr[] = {arr1, arr2};list(arr, 2, 4);
}void list(int **arr, int max_i, int max_j){int i,j;for(i=0; i<max_i; i++){for(j=0; j<max_j; j++){int x = arr[i][j];printf("%i, ", x);}printf("\n");}
}


用字符串类型的话,初始化部分变得相当简单,因为它允许直接初始化指向字符串的指针。

const char *strings[] = {"one","two","three"
};


但这有个陷阱,字符串实例被转换成指针,用 sizeof 操作符时会返回指针大小,而不是整个字符串文本所占空间。另一个重要区别是,若直接用指针修改字符串内容,则此行为是未定义的。

假设你能使用变长数组,那就有了第三种传多维数组给函数的方法。使用前面定义的变量来指定最里面数组的维度,变量 arr 变为一个指针,指向未完成的int数组。

void list(int max_i, int max_j, int arr[][max_j]){/* ... */int x = arr[1][3];
}

此方法对更高维度的数组仍然有效,因为第一维总是被转换为指向数组的指针。类似的规则同样作用于函数指示器。若函数指示器不是 sizeof 或一元操作符 & 的参数,它的值是一个指向函数的指针。这就是我们传回调函数时不需要 & 操作符的原因。

static void catch_int(int no) {/* ... */
};int main(){signal(SIGINT, catch_int);/* ... */
}


四、打桩(Interpositioning)

打桩是一种用定制的函数替换链接库函数且不需重新编译的技术。甚至可用此技术替换系统调用(更确切地说,库函数包装系统调用)。可能的应用是沙盒、调试或性能优化库。为演示过程,此处给出一个简单库,以记录GNU/Linux中 malloc 调用次数。

/* _GNU_SOURCE is needed for RTLD_NEXT, GCC will not define it by default */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <stdint.h>
#include <inttypes.h>static uint32_t malloc_count = 0;
static uint64_t total = 0;void summary(){fprintf(stderr, "malloc called: %u times\n", count);fprintf(stderr, "total allocated memory: %" PRIu64 " bytes\n", total);
}void *malloc(size_t size){static void* (*real_malloc)(size_t) = NULL;void *ptr = 0;if(real_malloc == NULL){real_malloc = dlsym(RTLD_NEXT, "malloc");atexit(summary);}count++;total += size;return real_malloc(size);
}


打桩要在链接libc.so之前加载此库,这样我们的 malloc 实现就会在二进制文件执行时被链接。可通过设置 LD_PRELOAD 环境变量为我们想让链接器优先链接的全路径。这也能确保其他动态链接库的调用最终使用我们的 malloc 实现。因为我们的目标只是记录调用次数,不是真正地实现内存分配,所以我们仍需要调用“真正”的 malloc 。通过传递 RTLD_NEXT 伪处理程序到 dlsym,我们获得了指向下一个已加载的链接库中 malloc 事件的指针。第一次 malloc 调用 libc 的 malloc,当程序终止时,会调用由 atexit 注册的获取和 summary 函数。看GNU/Linxu中打桩行为(真的184次调用!):

$ gcc -shared -ldl -fPIC malloc_counter.c -o /tmp/libmcnt.so
$ export LD_PRELOAD="/tmp/libstr.so"
$ psPID TTY          TIME CMD2758 pts/2    00:00:00 bash4371 pts/2    00:00:00 ps
malloc called: 184 times
total allocated memory: 302599 bytes


4.1 符号可见性

默认情况下,所有的非静态函数可被导出,所有可能仅定义有着与其他动态链接库函数甚至模板文件相同特征标的函数,就可能在无意中插入其它名称空间。为防止意外打桩、污染导出的函数名称空间,有效的做法是把每个函数声明为静态的,此函数在目标文件之外不能被使用。

在共享库中,另一种控制导出的共享目标的方式是用编译器扩展。GCC 4.x和Clang都支持 visibility属性和 -fvisibility 编译命令来对每个目标文件设置全局规则。其中 default 意味着不修改可见性,hidden 对可见性的影响与 static 限定符相同。此符号不会被放入动态符号表,其他共享目标或可执行文件看不到此符号。

#if __GNUC__ >= 4 || __clang__#define EXPORT_SYMBOL __attribute__ ((visibility ("default")))#define LOCAL_SYMBOL  __attribute__ ((visibility ("hidden")))
#else#define EXPORT_SYMBOL#define LOCAL_SYMBOL
#endif


全局可见性由编译器参数指定,可通过设置 visibility 属性被本地覆盖。实际上,全局策略设置为 hidden,则所有符号会被默认为本地的,只有修饰 __attribute__ ((visibility (“default”))) 将被导出。


这篇关于C进阶指南(2):数组和指针、打桩的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/920267

相关文章

JDK21对虚拟线程的几种用法实践指南

《JDK21对虚拟线程的几种用法实践指南》虚拟线程是Java中的一种轻量级线程,由JVM管理,特别适合于I/O密集型任务,:本文主要介绍JDK21对虚拟线程的几种用法,文中通过代码介绍的非常详细,... 目录一、参考官方文档二、什么是虚拟线程三、几种用法1、Thread.ofVirtual().start(

从基础到高级详解Go语言中错误处理的实践指南

《从基础到高级详解Go语言中错误处理的实践指南》Go语言采用了一种独特而明确的错误处理哲学,与其他主流编程语言形成鲜明对比,本文将为大家详细介绍Go语言中错误处理详细方法,希望对大家有所帮助... 目录1 Go 错误处理哲学与核心机制1.1 错误接口设计1.2 错误与异常的区别2 错误创建与检查2.1 基础

JavaScript对象转数组的三种方法实现

《JavaScript对象转数组的三种方法实现》本文介绍了在JavaScript中将对象转换为数组的三种实用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友... 目录方法1:使用Object.keys()和Array.map()方法2:使用Object.entr

使用Java填充Word模板的操作指南

《使用Java填充Word模板的操作指南》本文介绍了Java填充Word模板的实现方法,包括文本、列表和复选框的填充,首先通过Word域功能设置模板变量,然后使用poi-tl、aspose-words... 目录前言一、设置word模板普通字段列表字段复选框二、代码1. 引入POM2. 模板放入项目3.代码

macOS彻底卸载Python的超完整指南(推荐!)

《macOS彻底卸载Python的超完整指南(推荐!)》随着python解释器的不断更新升级和项目开发需要,有时候会需要升级或者降级系统中的python的版本,系统中留存的Pytho版本如果没有卸载干... 目录MACOS 彻底卸载 python 的完整指南重要警告卸载前检查卸载方法(按安装方式)1. 卸载

Rust 智能指针的使用详解

《Rust智能指针的使用详解》Rust智能指针是内存管理核心工具,本文就来详细的介绍一下Rust智能指针(Box、Rc、RefCell、Arc、Mutex、RwLock、Weak)的原理与使用场景,... 目录一、www.chinasem.cnRust 智能指针详解1、Box<T>:堆内存分配2、Rc<T>:

C++中处理文本数据char与string的终极对比指南

《C++中处理文本数据char与string的终极对比指南》在C++编程中char和string是两种用于处理字符数据的类型,但它们在使用方式和功能上有显著的不同,:本文主要介绍C++中处理文本数... 目录1. 基本定义与本质2. 内存管理3. 操作与功能4. 性能特点5. 使用场景6. 相互转换核心区别

Python动态处理文件编码的完整指南

《Python动态处理文件编码的完整指南》在Python文件处理的高级应用中,我们经常会遇到需要动态处理文件编码的场景,本文将深入探讨Python中动态处理文件编码的技术,有需要的小伙伴可以了解下... 目录引言一、理解python的文件编码体系1.1 Python的IO层次结构1.2 编码问题的常见场景二

Oracle Scheduler任务故障诊断方法实战指南

《OracleScheduler任务故障诊断方法实战指南》Oracle数据库作为企业级应用中最常用的关系型数据库管理系统之一,偶尔会遇到各种故障和问题,:本文主要介绍OracleSchedul... 目录前言一、故障场景:当定时任务突然“消失”二、基础环境诊断:搭建“全局视角”1. 数据库实例与PDB状态2

Git进行版本控制的实战指南

《Git进行版本控制的实战指南》Git是一种分布式版本控制系统,广泛应用于软件开发中,它可以记录和管理项目的历史修改,并支持多人协作开发,通过Git,开发者可以轻松地跟踪代码变更、合并分支、回退版本等... 目录一、Git核心概念解析二、环境搭建与配置1. 安装Git(Windows示例)2. 基础配置(必